from datetime import datetime
import json
import logging
import re
import subprocess
import time
import uuid
from typing import Any, Optional, Union, cast
from zoneinfo import available_timezones
from flask_babel  import _
from flask_restful import fields  # type: ignore
from core.file import helpers as file_helpers

from extensions.ext_redis import redis_client
from models.enums import Gender


class TokenManager:
    @classmethod
    def generate_token(
        cls,
        token_type: str,
        account: Optional["Account"] = None,
        email: Optional[str] = None,
        additional_data: Optional[dict] = None,
    ) -> str:
        if account is None and email is None:
            raise ValueError(_("Account or email must be provided"))

        account_id = account.id if account else None
        account_email = account.email if account else email

        if account_id:
            old_token = cls._get_current_token_for_account(account_id, token_type)
            if old_token:
                if isinstance(old_token, bytes):
                    old_token = old_token.decode("utf-8")
                cls.revoke_token(old_token, token_type)

        token = str(uuid.uuid4())
        token_data = {"account_id": account_id, "email": account_email, "token_type": token_type}
        if additional_data:
            token_data.update(additional_data)

        expiry_minutes = app_config.model_dump().get(f"{token_type.upper()}_TOKEN_EXPIRY_MINUTES")
        if expiry_minutes is None:
            raise ValueError(_("Expiry minutes for {token_type} token is not set").format(token_type=token_type))
        token_key = cls._get_token_key(token, token_type)
        expiry_time = int(expiry_minutes * 60)
        redis_client.setex(token_key, expiry_time, json.dumps(token_data))

        if account_id:
            cls._set_current_token_for_account(account_id, token, token_type, expiry_minutes)

        return token

    @classmethod
    def _get_token_key(cls, token: str, token_type: str) -> str:
        return f"{token_type}:token:{token}"

    @classmethod
    def revoke_token(cls, token: str, token_type: str):
        token_key = cls._get_token_key(token, token_type)
        redis_client.delete(token_key)

    @classmethod
    def get_token_data(cls, token: str, token_type: str) -> Optional[dict[str, Any]]:
        key = cls._get_token_key(token, token_type)
        token_data_json = redis_client.get(key)
        if token_data_json is None:
            logging.warning(f"{token_type} token {token} not found with key {key}")
            return None
        token_data: Optional[dict[str, Any]] = json.loads(token_data_json)
        return token_data

    @classmethod
    def _get_current_token_for_account(cls, account_id: str, token_type: str) -> Optional[str]:
        key = cls._get_account_token_key(account_id, token_type)
        current_token: Optional[str] = redis_client.get(key)
        return current_token

    @classmethod
    def _set_current_token_for_account(
        cls, account_id: str, token: str, token_type: str, expiry_hours: Union[int, float]
    ):
        key = cls._get_account_token_key(account_id, token_type)
        expiry_time = int(expiry_hours * 60 * 60)
        redis_client.setex(key, expiry_time, token)

    @classmethod
    def _get_account_token_key(cls, account_id: str, token_type: str) -> str:
        return f"{token_type}:account:{account_id}"


class StrLen:
    """Restrict input to an integer in a range (inclusive)"""

    def __init__(self, max_length, argument="argument"):
        self.max_length = max_length
        self.argument = argument

    def __call__(self, value):
        length = len(value)
        if length > self.max_length:
            error = "Invalid {arg}: {val}. {arg} cannot exceed length {length}".format(
                arg=self.argument, val=value, length=self.max_length
            )
            raise ValueError(error)

        return value
    
# lobyliang
def get_icon_url(icon_type, icon)->Optional[str]:
    if not icon:
        return None
    # 检查icon是否是合法的UUID
    try:
        from models.enums import IconType

        if icon_type == IconType.IMAGE.value:
            uuid.UUID(icon, version=4)
            return file_helpers.get_signed_file_url(icon)
        else:
            return None
    except ValueError:
        pass
    return None

class RateLimiter:
    def __init__(self, prefix: str, max_attempts: int, time_window: int):
        self.prefix = prefix
        self.max_attempts = max_attempts
        self.time_window = time_window

    def _get_key(self, email: str) -> str:
        return f"{self.prefix}:{email}"

    def is_rate_limited(self, email: str) -> bool:
        key = self._get_key(email)
        current_time = int(time.time())
        window_start_time = current_time - self.time_window

        redis_client.zremrangebyscore(key, "-inf", window_start_time)
        attempts = redis_client.zcard(key)

        if attempts and int(attempts) >= self.max_attempts:
            return True
        return False

    def increment_rate_limit(self, email: str):
        key = self._get_key(email)
        current_time = int(time.time())

        redis_client.zadd(key, {current_time: current_time})
        redis_client.expire(key, self.time_window * 2)


def timezone(timezone_string):
    if timezone_string and timezone_string in available_timezones():
        return timezone_string

    error = _("{timezone_string} is not a valid timezone.").format(timezone_string=timezone_string)
    raise ValueError(error)



class DatetimeString:
    def __init__(self, format, argument="argument"):
        self.format = format
        self.argument = argument

    def __call__(self, value):
        try:
            datetime.strptime(value, self.format)
        except ValueError:
            error = "Invalid {arg}: {val}. {arg} must be conform to the format {format}".format(
                arg=self.argument, val=value, format=self.format
            )
            raise ValueError(error)

        return value

def StrBoolNone(value=None)->bool|None:
    if value is None:
        return None
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        if value is None or value == "":
            return None
        if value.strip().lower() in ["true", "1", "yes"]:
            return True
        elif value.strip().lower() in ["false", "0", "no"]:
            return False
    return None
    
def StrBool(value):
    if isinstance(value, bool):
        return value
    if value is None or value == "":
        return None
    if value.strip().lower() in ["true", "1", "yes"]:
        return True
    elif value.strip().lower() in ["false", "0", "no"]:
        return False
    else:
        error = "Invalid parameter: {val}. must be a boolean.".format(
            val=value,
        )
        raise ValueError(error)
    
def extract_remote_ip(request) -> str:
    if request.headers.get("CF-Connecting-IP"):
        return cast(str, request.headers.get("Cf-Connecting-Ip"))
    elif request.headers.getlist("X-Forwarded-For"):
        return cast(str, request.headers.getlist("X-Forwarded-For")[0])
    else:
        return cast(str, request.remote_addr)
 
class TimestampField(fields.Raw):
    def format(self, value) -> int:
        return int(value.timestamp())

class GenderField(fields.Raw):
    def format(self, value:Gender) -> str:
        if value == Gender.MALE:
            return _("male")
        return _("female")

def GenderStr(value:Gender)->str:
    if value == Gender.MALE:
            return _("male")
    return _("female")

def email(email):
    # Define a regex pattern for email addresses
    pattern = r"^[\w\.!#$%&'*+\-/=?^_`{|}~]+@([\w-]+\.)+[\w-]{2,}$"
    # Check if the email matches the pattern
    if re.match(pattern, email) is not None:
        return email

    error = _("{email} is not a valid email.").format(email=email)
    raise ValueError(error)

def gender_value(value):
    if isinstance(value, int):
        if value in [0, 1]:
            return Gender(value)
    elif isinstance(value, str):
        if value in ["0", "1"]:
            return Gender(int(value))
    raise ValueError(_("{value} is not a valid gender.").format(value=value))
        
def uuid_value(value):
    if value == "":
        return str(value)

    try:
        uuid_obj = uuid.UUID(value)
        return str(uuid_obj)
    except ValueError:
        error = _("{value} is not a valid uuid.").format(value=value)
        raise ValueError(error)   
    
def run(script):
    return subprocess.getstatusoutput("source /root/.bashrc && " + script)
